﻿//
// Created by LiuYou on 2021/4/8.
//

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/**
 * @brief 二维数组
 *      * 一般形式
 *              TODO 注意: 一般形式的二维数组数组名是不能进行自增运算，因为数组名是地址常量！
 *      * 行指针形式的二维数组
 *          行指针形式的二维数组的数组名能否进行自增运算？
 *              TODO 注意: 很重要 行指针形式的数组名是可以进行自增运算的，因为行指针形式的数组，指针是一个变量。
 */
void doubleArray() {
    int doubleArray[4][4] = {0};
    int (* ptrDoubleArray)[4] = (int (*)[4]) malloc(4 * 4 * sizeof(int));
    ptrDoubleArray[0][0] = 1;
    ptrDoubleArray[1][3] = 100;
    printf("%d\n", doubleArray[3][3]);
    printf("%d\n", *(*(++ptrDoubleArray) + 3));
    // 行！
    ++ptrDoubleArray;
    // 不行
//    ++doubleArray;

}

/**
 * @brief 指针数组
 */
void pointerArray() {
    // 将字符串存储在字符数组中，然后再通过指针数组存储每个字符串的首地址。
    char ch[4][7] = {"123", "456", "789", "101112"};
    char* ptrCh[4];
    for (int i = 0; i < 4; ++i) {
        ptrCh[i] = ch[i];
        // 或者
        // 虽然有一些警告，但是可以通过编译且正常运行。
//        ptrCh[i] = ch + i;
    }
    for (int i = 0; i < 4; ++i) {
        printf("%s\n", ptrCh[i]);
    }


    // 虽然这里初始化的字符串也像数组那样存储在内存中的默写存储单元中，
    // 但是编译器只需将这些字符串的存储单元的首地址赋值给ptrStr中的元素，
    // 而无需知道存储这些字符串的数组名字是什么。
    /// 这里真的很重要！！！！
    // 而且使用这种使用字符串常量对指针数组元素进行初始化时，由于每个字符串常量
    // 在内存中所占的存储空间大小与实际长度相同，因此，在这种情况下可以节省内存！！！！
    char* ptrStr[4] = {"123", "456", "789", "101112"};
    for (int i = 0; i < 4; ++i) {
        printf("%s\n", ptrStr[i]);
    }

    // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    /*
     int main(int argc, char* argv[]) {
        // ...
        return 0;
     }
     argc: argument count
     存放命令行中参数的个数
     argv: argument value
     用来接收命令行参数
     */
    // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++



}

/**
 * @brief 指针和二维数组
 * 从行指针和列指针的角度切入是最容易理解的同时也是最实用的思考方式！！！！
 */
void pointerAndDoubleArray() {
    char ch[2][8] = {"liuyou", "chenwei"};
    printf("%s\n", ch); // liuyou
    printf("%s\n", ch + 1); // chenwei
    printf("%s\n", ch[0]); // liuyou
    printf("%s\n", &ch[0][0]); // liuyou
    printf("%s\n", ch[1]); // chenwei
    //    printf("%s\n", ch[0][0]); // 错误，因为不是地址


    // 输出 u 和 w
    printf("%c\n", *(*ch + 2));
    printf("%c\n", *(*(ch + 1) + 4));
    printf("%s\n", *ch); // liuyou

    // 输出 o 和 h
    printf("%c\n", *(ch[0] + 4));
    printf("%c\n", (*(ch + 1))[1]);
}

void pointerAndArray() {
    char ch[10] = "123456789";
    char* ptr = ch;
    printf("%s\n", ch);
    // ch + 1后从2开始
    printf("%s\n", ch + 1);
    printf("%s\n", ptr);
    // ptr + 1后也是从2开始
    printf("%s\n", ptr + 1);
    // 总结: ptr+i 并不是将指针变量的值加1，而是加上 i*sizeof(指针类型) 个字节！！！！


    // ******************************************************************
    // 指针的下标法，非常有用！！！！
    char c[4] = "123";
    // 等价于 char* ptrC = &c[0];
    char* ptrC = c;
    printf("%s\n", ptrC); // 输出123
    printf("%c\n", *ptrC); // 输出1

    // 指针的下标法
    printf("%c\n", *(ptrC + 2)); // 输出3
    printf("%c\n", ptrC[2]); // 输出3
    // ptrC[2] 相当于 *(ptrC + 2)
    // 指针的下标法
    printf("%s\n", &ptrC[1]); // 输出23
    printf("%s\n", ptrC + 1); // 输出23
    // &ptrC[1] 相当于 ptrC + 1

    // ******************************************************************
}

/**
 * @brief C语言字符串处理函数
 * 需要包含 <string.h>
 */
void stringFunctions() {
    char* ptr = "hello";
    // 字符串长度
    printf("%d", strlen(ptr));

    // 字符串复制，注意: 一定要是字符数组！！！！
    // 而且大小要放得下字符串！！！！
    // 指针只是地址并不是变量对象，而是变量对象的地址！！！！
    // 在这里要存储字符串还是必须要用到字符数组而不是字符指针，然后字符指针可以指向字符数组，进而来表示字符串！
#pragma region Error
//    char* ptr2 = NULL;
//    strcpy(ptr2, ptr);
//    printf("%s", ptr2);
#pragma endregion
    // 注意大小要放得下
    char str[10];
    strcpy(str, ptr);
    printf("%s\n", str);

    // 字符串连接
    // strcat(str1,str2);
    // 将str2添加到str1字符串的末尾，而且str2的第一个字符将str1的'\0'字符覆盖！
    // 所以要保证str1足够大
    char str1[100] = "hello ni hao";
    char str2[10] = "bu hao";
    strcat(str1, str2);
    printf("%s", str1);

    // 在C语言中赋值运算符不能用于字符串的赋值操作，字符串赋值只能通过函数, 例如: strcpy()等

    // 在C语言中字符串首先是要通过字符型数组进行存储，而且存储在常量存储区中，字符串是只读的！字符指针仅仅是用来表示字符串的！
    // 真正字符串复制的时候并不是复制字符串首地址(字符指针的值)，而是存储该字符串的字符型数组！！！！

    // =================================================================
    char c[10] = "hello";
    char* ptrToC = c;
// 下面两行代码报错！
//    ++c;
//    c++;
    ++ptrToC;
    ptrToC++;

    // 这里就能得出结论:
    // 数组名是地址常量并不是左值，所以不能进行自增、自减这种加减后又改变自身的操作。但是可以进行算术运算(数组名+i 或者 数组名-i)，因为
    // 这样并不改变地址常量自身！！！！
    // 而通过一个字符指针指向该字符串后，字符指针是指针是可以进行自增自减这种能改变自身的值的操作，算术运算更是可以。

    // =================================================================


    // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // 常用的字符串转换函数(以下三个函数需要包含头文件 <stdlib.h>)
    char c1[10] = "3.1415";
    double value1 = atof(c1);
    printf("%f\n", value1);

    char c2[10] = "1024";
    int value2 = atoi(c2);
    printf("%d\n", value2);

    char c3[100] = "10002032";
    long value3 = atol(c3);
    printf("%d\n", value3);

    // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
}

/**
 * @brief 字符串
 * C语言没有提供专门的字符串数据类型。
 * 所以字符串要使用到字符型数组(character array)来实现。
 */
void characterString() {
    // 使用character array来表达字符串
    char string[] = {"Hello1"};
    printf("%s", string);

    // character pointer
    // 因为字符串在内存中都占用一段连续的存储空间，并且有唯一确定的首地址，而且每个字符串的结尾都是字符'\0'。
    // 所以可以通过指针指向这样的地址来表达一个字符串但是不能通过这个指针来修改字符串。因为字符串存储在只读的常量存储区中！！！！
    // 但是指针可以改变指向去指另外的字符串。
    char* ptr = "Hello2";
    printf("%s", ptr);

    ptr = string;
    printf("%s", ptr);
    // 或者
    ptr = &string[0];
    printf("%s", ptr);

    // 注意！！！！！！！！！！！！！！！
    // 因为数组名是一个地址常量，所以不会有 string++ 或者 ++string 此类表达式，因为 string 是一个地址常量，其值不能修改！
}

void method() {
    printf("==================\n");
}

/**
 * @brief 函数指针
 * 函数指针更加灵活，只要函数的返回值类型和参数的个数和类型相同的函数，就可以通过
 * 定义一个这样的函数指针指向这些函数。
 * 例: 函数指针可以用在一些函数的参数中。
 */
void pointerToFunction() {
    // 函数指针定义
    void (* ptrToFunc)() =method;
    // ptrToFunc是一个函数指针，指向了method方法
    // 通过函数指针调用函数method
    (*ptrToFunc)();
    // 直接调用函数method
    method();
}

/**
 * @brief 用来测试各个函数
 * @return 0
 */
int main(int argc, char** argv) {
    pointerToFunction();
    printf("\n");

    characterString();
    printf("\n");

    stringFunctions();
    printf("\n");

    pointerAndArray();
    printf("\n");

    pointerAndDoubleArray();
    printf("\n");

    pointerArray();
    printf("\n");


    // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // 补充 argc argv
    printf("===========================================\n");
    printf("%d\n", argc);
    // 因为命令行没有其它参数，所以argc只有一个即可执行文件的绝对地址。
    // 可以在编译的命令行加上其它参数，在CLion中进行如下操作:
    // 在锤子后面的 Edit Configurations 中的 program arguments输入即可。
    // 例现在我输入了 hello 。此时编译运行该程序。argc变为2。并将绝对地址和hello字符串打印出来。
    for (int i = 0; i < argc; ++i) {
        // 打印编译链接生成的可执行文件的绝对地址。
//        printf("%s\n", argv[i]);
        printf("%s\n", *(argv + i));
    }
    // 报错
    //    printf("%s\n", argv[argc + 1]);
    // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    // =========================================================================
    doubleArray();
    // =========================================================================

    return 0;
}